﻿
namespace EOS.Framework.Data
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using EOS.Framework.Data.Rules;

    public class BaseDomainObject : INotifyPropertyChanged, IEditableObject, IDataErrorInfo
    {
        private Dictionary<string, object> properties;
        private Dictionary<string, string> messages = new Dictionary<string, string>();
        private List<Rule> rules;
        private bool inTransaction = false;
        private bool isChanged;

        public virtual event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Gets a value indicating whether or not this domain object is valid. 
        /// </summary>
        public virtual bool IsValid
        {
            get { return string.IsNullOrEmpty(Error); }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is changed.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is changed; otherwise, <c>false</c>.
        /// </value>
        public virtual bool IsChanged
        {
            get { return isChanged; }
        }

        #region Validating

        /// <summary>
        /// Override this method to create your own rules to validate this business object. These rules must all be met before 
        /// the business object is considered valid enough to save to the data store.
        /// </summary>
        /// <returns>A collection of rules to add for this business object.</returns>
        protected virtual List<Rule> CreateRules()
        {
            return new List<Rule>();
        }

        /// <summary>
        /// A helper method that raises the PropertyChanged event for a property.
        /// </summary>
        /// <param name="propertyNames">The names of the properties that changed.</param>
        protected virtual void NotifyPropertyChanged(string name, object value)
        {
            if (inTransaction)
            {
                isChanged = true;
            }

            messages[name] = null;

            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }

        /// <summary>
        /// Validates the specified name.
        /// </summary>
        /// <param name="name">The name.</param>
        private string Validate(string name)
        {
            object value = GetType().GetProperty(name).GetValue(this, null);

            if (rules == null)
            {
                rules = new List<Rule>();
                rules.AddRange(CreateRules());
            }
            
            var rulesQuery = from r in rules where r.PropertyName == name select r;

            string message = string.Empty;
            foreach (Rule rule in rulesQuery)
            {
                bool isRuleBroken = !rule.ValidateRule(value);
                if (isRuleBroken)
                {
                    message += rule.Description;
                    message += Environment.NewLine;
                }
            }
            messages[name] = message;
            return message;
        }

        #endregion

        #region INotifyPropertyChanged Member

        /// <summary>
        /// Starts editing the object.
        /// </summary>
        public virtual void BeginEdit()
        {
            if (!inTransaction)
            {
                PropertyInfo[] propertyInfos = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
                properties = new Dictionary<string, object>(propertyInfos.Length - 1);

                foreach (PropertyInfo propertyInfo in propertyInfos)
                {
                    if (propertyInfo.Name != "Error" && 
                        propertyInfo.Name != "IsValid" &&
                        propertyInfo.Name != "IsChanged" &&
                        propertyInfo.Name != "Item")
                    {
                        object value = propertyInfo.GetValue(this, null);

                        if (propertyInfo.CanWrite && propertyInfo.PropertyType.IsSubclassOf(typeof(IEnumerable)) == false)
                        {
                            properties.Add(propertyInfo.Name, value);
                        }
                    }
                }
                inTransaction = true;
            }
        }

        /// <summary>
        /// Cancel the changes since the last all of <see cref="M:System.ComponentModel.IEditableObject.BeginEdit"/>.
        /// </summary>
        public virtual void CancelEdit()
        {
            if (inTransaction)
            {
                if (null == properties)
                {
                    return;
                }

                PropertyInfo[] propertyInfos = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
                foreach (PropertyInfo propertyInfo in propertyInfos)
                {
                    if (propertyInfo.Name != "Error" && 
                        propertyInfo.Name != "IsValid" &&
                        propertyInfo.Name != "IsChanged" && 
                        propertyInfo.Name != "Item")
                    {
                        if (propertyInfo.PropertyType.IsSubclassOf(typeof(IEnumerable)) == false)
                        {
                            propertyInfo.SetValue(this, properties[propertyInfo.Name], null);
                        }
                    }
                }
                EndEdit();
            }
        }

        /// <summary>
        /// Finished editing the object.
        /// </summary>
        public virtual void EndEdit()
        {
            if (inTransaction)
            {
                properties = null;
                isChanged = false;
                inTransaction = false;
            }
        }

        #endregion

        #region IDataErrorInfo Member

        /// <summary>
        /// Gets an error message indicating what is wrong with this object.
        /// </summary>
        /// <value>The error message.</value>
        /// <returns>An error message indicating what is wrong with this object. 
        /// The default is an empty string ("").</returns>
        public virtual string Error
        {
            get { return GetErrorMessage().Trim(); }
        }

        /// <summary>
        /// Gets the error message for the property with the given name.
        /// </summary>
        /// <param name="propertyName">The name of the property whose error message to get.</param>
        /// <returns>The error message for the property. The default is an empty string ("").</returns>
        public virtual string this[string propertyName]
        {
            get { return GetErrorMessage(propertyName); }
        }

        /// <summary>
        /// Gets the error message.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <returns></returns>
        private string GetErrorMessage(string property)
        {
            return Validate(property);
        }

        /// <summary>
        /// Gets the error messages.
        /// </summary>
        /// <returns></returns>
        private string GetErrorMessage()
        {
            var customProperties = from p in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                   where p.Name != "Error" &&
                                         p.Name != "IsValid" &&
                                         p.Name != "IsChanged" &&
                                         p.Name != "Item"
                                   select p;

            string message = string.Empty;
            foreach (PropertyInfo propertyInfo in customProperties)
            {
                string propertyErrorMessage = Validate(propertyInfo.Name);
                if (!string.IsNullOrEmpty(propertyErrorMessage))
                {
                    message += propertyErrorMessage;
                    message += Environment.NewLine;
                }
            }

            return message;
        }

        #endregion
    }
}
